package Basic;

import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Vector;

import Game.Blackboard;

/**
 * 
 * @author egg Classe que representa a visao em Tiles dos agentes
 * 
 */
public class Tiles {

	private int width; // Largura do mapa
	private int height; // Altura do mapa
	private int noTilesW; // Numero de tiles onde a largura sera subdividida
	private int noTilesH; // Numero de tiles onde a altura sera subdividida

	// As constantes que representam o tipo do obstaculo
	private final int NORMAL = 0; // Podemos ir para qualquer um dos 8 quadrados
	// vizinhos
	private final int OBSTACLE = 1; // Obstaculo, nao podemos ir para este tile

	private int[] obstacles; // Vetor de inteiros que contem o tipo de obstaculo

	private Blackboard blackboard;

	// para cada tile.

	/**
	 * 
	 * Construtor
	 * 
	 * @param width
	 * @param height
	 * @param noTilesW
	 * @param noTilesH
	 * @param obstacles
	 */
	public Tiles(int width, int height, int noTilesW, int noTilesH, StaticObstacle[] obstacles, Blackboard blackboard) {
		this.width = width;
		this.height = height;
		this.noTilesW = noTilesW;
		this.noTilesH = noTilesH;
		this.obstacles = new int[this.noTilesW * this.noTilesH];
		for (int i = 0; i < obstacles.length; i++) {
			this.addObstacle(obstacles[i]);
		}
		this.blackboard = blackboard;
	}

	/**
	 * Dado um ponto p, ele retorna o ponto central do tile no qual aquele ponto
	 * esta contido
	 * 
	 * @param p
	 * @return O ponto central do tile no qual p esta contido
	 */
	public Point getCentral(Point p) {
		return this.getCentral(this.getTile(p.getX(), p.getY()));
	}

	/**
	 * Retorna o ponto central do tile passado como parametro
	 * 
	 * @param tile
	 * @return O ponto central do tile
	 */
	public Point getCentral(int tile) {
		int w = tile % this.noTilesW;
		int h = (tile - w) / this.noTilesW;
		double wm = (double) w + 0.5;
		double hm = (double) h + 0.5;
		Point ret = new Point((int) (wm * ((double) this.width / (double) this.noTilesW)), (int) (hm * ((double) this.height / (double) this.noTilesH)));
		return ret;
	}

	/**
	 * Retorna o tile no qual o ponto p esta contido
	 * 
	 * @param p
	 * @return - O tile no qual p esta contido
	 */
	public int getTile(int pointX, int pointY) {
		int x = 0;
		int y = 0;
		if (pointX >= this.width)
			x = this.width - 1;
		else
			x = pointX;
		if (pointY >= this.height)
			y = this.height - 1;
		else
			y = pointY;
		int w = x / (this.width / this.noTilesW);
		int h = y / (this.height / this.noTilesH);
		return ((w * 1) + (h * this.noTilesW));
	}

	/**
	 * Retorna um vetor com todos os tiles vizinhos do tile passado como
	 * parametro
	 * 
	 * @param tile
	 * @return Todos os vizinhos possiveis ou nao
	 */
	private ArrayList<Integer> getAllNeighborsTiles(int tile) {

		ArrayList<Integer> ret = new ArrayList<Integer>();

		// Vizinhos de cima
		if (tile / this.noTilesW >= 1) {
			ret.add(tile - (this.noTilesW)); // Vizinho exatamente acima
		}

		// Vizinhos do lado
		if (tile % this.noTilesW != 0)
			ret.add(tile - 1); // Vizinho da esquerda
		if ((tile + 1) % this.noTilesW != 0)
			ret.add(tile + 1); // Vizinho da direita

		// Vizinhos de baixo
		if (tile / this.noTilesW < (this.noTilesH - 1)) {
			ret.add(tile + (this.noTilesW)); // Vizinho exatamente abaixo
		}

		return ret;
	}

	/**
	 * Retorna um vetor com todos os tiles vizinhos possiveis do tile passado
	 * como parametro
	 * 
	 * @param tile
	 * @return Todos os vizinhos possiveis (que nao sao obstaculo, nem sao fora
	 *         do mapa) do tile
	 */
	public ArrayList<Integer> getNeighborsTiles(int tile) {

		ArrayList<Integer> ret = new ArrayList<Integer>();

		// Vizinhos de cima
		if (tile / this.noTilesW >= 1) {
			if (this.obstacles[tile - (this.noTilesW)] != this.OBSTACLE)
				ret.add(tile - (this.noTilesW));
		}

		// Vizinhos do lado
		if (tile % this.noTilesW != 0 && this.obstacles[tile - 1] != this.OBSTACLE)
			ret.add(tile - 1);
		if ((tile + 1) % this.noTilesW != 0 && this.obstacles[tile + 1] != this.OBSTACLE)
			ret.add(tile + 1);

		// Vizinhos de cima
		if (tile / this.noTilesW < (this.noTilesH - 1)) {
			if (this.obstacles[tile + (this.noTilesW)] != this.OBSTACLE)
				ret.add(tile + (this.noTilesW));
		}

		return ret;
	}

	/**
	 * Retorna o numero total de tiles no qual o mapa foi subdividido
	 * 
	 * @return numero total de tiles
	 */
	public int getTotalTiles() {
		return this.noTilesW * this.noTilesH;
	}

	/**
	 * Chama o A* para dois pontos
	 * 
	 * @param start
	 *            (inicio do A*)
	 * @param goal
	 *            (objetivo do A*)
	 * @return Um vetor de pontos com o caminho INVERSO entre start e goal -
	 *         Para calcular o caminho correto basta mudar a ordem dos
	 *         parametros. Nao retorna (no caminho) o start e o goal.
	 */
	public Vector<Point> aStar(int xStart, int yStart, int xGoal, int yGoal) {
		return this.aStar(this.getTile(xStart, yStart), this.getTile(xGoal, yGoal));
	}

	public Vector<Point> aStar(int xStart, int yStart, int xGoal, int yGoal, Vector<Point> path) {
		if (path.size() == 0) {
			path = aStar(xStart, yStart, xGoal, yGoal);
			if (path.size() > 2) {
				path.subList(2, path.size() - 1).clear();
			}
		} else {
			path.remove(0);
		}
		return path;
	}

	public Vector<Point> aStar2(int xStart, int yStart, int xGoal, int yGoal) {
		return this.aStar(this.getTile(xStart, yStart), this.getTile(xGoal, yGoal));
	}

	/**
	 * Calcula o A* para dois tiles
	 * 
	 * @param start
	 *            (inicio do A*)
	 * @param goal
	 *            (objetivo do A*)
	 * @return Um vetor de pontos com o caminho INVERSO entre start e goal -
	 *         Para calcular o caminho correto basta mudar a ordem dos
	 *         parametros. Nao retorna (no caminho) o start e o goal.
	 */
	public Vector<Point> aStar(int start, int goal) {

		ArrayList<Integer> closedSet = new ArrayList<Integer>();
		// Vector<Integer> openSet = new Vector<Integer>();
		// BinaryHeap openSet = new BinaryHeap();
		PriorityQueue<Tuple> openSet = new PriorityQueue<Tuple>();

		double[] g = new double[this.getTotalTiles()];
		double[] h = new double[this.getTotalTiles()];
		double[] f = new double[this.getTotalTiles()];
		boolean[] isInOpenSet = new boolean[this.getTotalTiles()];
		boolean[] isInClosedSet = new boolean[this.getTotalTiles()];
		int[] cameFrom = new int[this.getTotalTiles()];
		for (int i = 0; i < cameFrom.length; i++) {
			cameFrom[i] = -1;
		}

		g[start] = 0;
		h[start] = this.euclidean(this.getCentral(start), this.getCentral(goal));
		f[start] = h[start];
		isInOpenSet[start] = true;
		openSet.add(new Tuple(start, f[start]));

		while (!openSet.isEmpty()) {

			int x = (Integer) openSet.remove().tile;
			isInOpenSet[x] = false;

			closedSet.add(x);
			isInClosedSet[x] = true;

			ArrayList<Integer> neighbors = this.getNeighborsTiles(x);
			for (int i = 0; i < neighbors.size(); i++) {

				int y = neighbors.get(i);

				if (!isInClosedSet[y]) {

					double tentativeG = g[x] + (this.width / this.noTilesW);

					if (!isInOpenSet[y]) {
						cameFrom[y] = x;
						g[y] = tentativeG;
						h[y] = this.euclidean(this.getCentral(y), this.getCentral(goal));
						f[y] = g[y] + h[y];
						openSet.add(new Tuple(y, f[y]));
						isInOpenSet[y] = true;
					} else if (tentativeG < g[y]) {
						cameFrom[y] = x;
						g[y] = tentativeG;
						h[y] = this.euclidean(this.getCentral(y), this.getCentral(goal));
						f[y] = g[y] + h[y];
					}
				}
			}
		}

		Vector<Point> ret = new Vector<Point>();
		int iter = goal;
		boolean noPath = false;
		while (cameFrom[iter] != start) {
			if (cameFrom[iter] == -1) {
				noPath = true;
				break;
			}
			iter = cameFrom[iter];
			ret.add(this.getCentral(iter));
			if (ret.size() > 1) {
				Point p1 = ret.get(ret.size() - 1);
				Point p2 = ret.get(ret.size() - 2);
			} else if (ret.size() == 1) {
				Point p1 = ret.get(0);
			}
		}

		if (noPath) {
			// System.out.println("There is no path!");
			return new Vector<Point>();
		} else {
			Point p1 = ret.get(ret.size() - 1);
			return ret;
		}
	}

	/**
	 * Remove um obstaculo comum - Obstacle, Resource e Building
	 * 
	 * @param so
	 *            - Obstaculo a ser removido
	 */
	public void removeObstacle(StaticObstacle so) {
		int tile1 = this.getTile(so.getX() - (so.getWidth() / 2), so.getY() - (so.getHeight() / 2));
		int tile2 = this.getTile(so.getX() - 1 + (so.getWidth() / 2), so.getY() - (so.getHeight() / 2));
		int tile3 = this.getTile(so.getX() - (so.getWidth() / 2), (so.getY() - 1) + (so.getHeight() / 2));
		int rangeW = tile2 - tile1 + 1;
		int rangeH = ((tile3 - tile1) / this.noTilesW) + 1;
		for (int m = 0; m < rangeH; m++) {
			for (int n = 0; n < rangeW; n++) {
				int tile = n + (m * this.noTilesW) + tile1;
				this.obstacles[tile] = this.NORMAL;
			}
		}
	}

	/**
	 * Adiciona um obstaculo comum - Obstacle, Resource e Building
	 * 
	 * @param so
	 *            - Obstaculo a ser adicionado
	 */
	public void addObstacle(StaticObstacle so) {
		int tile1 = this.getTile(so.getX() - (so.getWidth() / 2), so.getY() - (so.getHeight() / 2));
		int tile2 = this.getTile((so.getX() - 1) + (so.getWidth() / 2), so.getY() - (so.getHeight() / 2));
		int tile3 = this.getTile(so.getX() - (so.getWidth() / 2), (so.getY() - 1) + (so.getHeight() / 2));
		int rangeW = tile2 - tile1 + 1;
		int rangeH = ((tile3 - tile1) / this.noTilesW) + 1;
		for (int m = 0; m < rangeH; m++) {
			for (int n = 0; n < rangeW; n++) {
				int tile = n + (m * this.noTilesW) + tile1;
				this.obstacles[tile] = this.OBSTACLE;
			}
		}
	}

	/**
	 * Calcula a distancia euclideana entre dois pontos
	 * 
	 * @param p1
	 * @param p2
	 * @return A distancia euclideana entre p1 e p2
	 */
	public double euclidean(Point p1, Point p2) {
		return Math.sqrt(Math.pow((p1.getX() - p2.getX()), 2) + Math.pow((p1.getY() - p2.getY()), 2));
	}

	public static void main(String[] args) {

		/*
		 * StaticObstacle so1 = new StaticObstacle(600, 400, 400, 200);
		 * StaticObstacle so2 = new StaticObstacle(1150, 500, 300, 400);
		 * StaticObstacle so3 = new StaticObstacle(300, 750, 600, 100);
		 * StaticObstacle so4 = new StaticObstacle(600, 850, 600, 100);
		 * StaticObstacle[] socol = { so1, so2, so3, so4 };
		 * 
		 * Tiles tiles = new Tiles(1700, 1000, 17, 10, socol, blackboard);
		 * 
		 * ArrayList<Point> path = tiles.aStar(3, 155); for (int i = 0; i <
		 * path.size(); i++) {
		 * System.out.println(tiles.getTile(path.get(i).getX(),
		 * path.get(i).getY())); }
		 */

	}

	public int getNoTilesW() {
		return noTilesW;
	}

}
